"""
Your awesome Distance Vector router for CS 168

Based on skeleton code by:
  MurphyMc, zhangwen0411, lab352
"""

import sim.api as api
from cs168.dv import RoutePacket, \
                     Table, TableEntry, \
                     DVRouterBase, Ports, \
                     FOREVER, INFINITY

class DVRouter(DVRouterBase):

    # A route should time out after this interval
    ROUTE_TTL = 15

    # Dead entries should time out after this interval
    GARBAGE_TTL = 10

    # -----------------------------------------------
    # At most one of these should ever be on at once
    SPLIT_HORIZON = False
    POISON_REVERSE = True
    # -----------------------------------------------
    
    # Determines if you send poison for expired routes
    POISON_EXPIRED = False

    # Determines if you send updates when a link comes up
    SEND_ON_LINK_UP = False

    # Determines if you send poison when a link goes down


    POISON_ON_LINK_DOWN = False

    def __init__(self):
        """
        Called when the instance is initialized.
        DO NOT remove any existing code from this method.
        However, feel free to add to it for memory purposes in the final stage!
        """
        assert not (self.SPLIT_HORIZON and self.POISON_REVERSE), \
                    "Split horizon and poison reverse can't both be on"
        
        self.start_timer()  # Starts signaling the timer at correct rate.

        # Contains all current ports and their latencies.
        # See the write-up for documentation.
        self.ports = Ports()
        
        # This is the table that contains all current routes
        self.table = Table()
        self.table.owner = self


    def add_static_route(self, host, port):
        """
        Adds a static route to this router's table.

        Called automatically by the framework whenever a host is connected
        to this router.

        :param host: the host.
        :param port: the port that the host is attached to.
        :returns: nothing.
        """
        # `port` should have been added to `peer_tables` by `handle_link_up`
        # when the link came up.
        assert port in self.ports.get_all_ports(), "Link should be up, but is not."

        # TODO: fill this in!
        latency = self.ports.get_latency(port)
        self.table[host] = (TableEntry(dst=host, port=port, latency=latency, expire_time=FOREVER))
        
    def handle_data_packet(self, packet, in_port):
        """
        Called when a data packet arrives at this router.

        You may want to forward the packet, drop the packet, etc. here.

        :param packet: the packet that arrived.
        :param in_port: the port from which the packet arrived.
        :return: nothing.
        """
        # TODO: fill this in!
        # If the packet is a route packet, handle it accordingly

        # 从路由表中获取目的地址的路由条目
        entry = self.table.get(packet.dst)
        # 检查路由条目是否存在，且延迟小于 INFINITY
        if entry and entry.latency < INFINITY:
            # 转发数据包到指定的端口
            self.send(packet, entry.port)
        else:
            # 丢弃数据包（什么都不做）
            pass
    def send_routes(self, force=False, single_port=None):
        """
        Send route advertisements for all routes in the table.

        :param force: if True, advertises ALL routes in the table;
                      otherwise, advertises only those routes that have
                      changed since the last advertisement.
               single_port: if not None, sends updates only to that port; to
                            be used in conjunction with handle_link_up.
        :return: nothing.
        """
        # TODO: fill this in!
        # 仅处理 force=True 的情况
        if force:
            # 遍历路由表中的所有路由
            for dst, entry in self.table.items():
                # 创建路由包
                packet = RoutePacket(destination=dst, latency=entry.latency)
                
                # 获取要发送的端口列表
                if single_port is not None:
                    ports = [single_port]
                else:
                    ports = self.ports.get_all_ports()
                
                # 向所有端口发送路由包
                for port in ports:
                    # Skip sending to the port that sent the route
                    if self.SPLIT_HORIZON and entry.port == port:
                        continue 
                    # Poison reverse routes if enabled
                    elif self.POISON_REVERSE and entry.port == port:
                        poison_packet = RoutePacket(destination=dst, latency=INFINITY)
                    # 发送路由包
                        self.send(poison_packet, port)
                    else:
                        self.send(packet, port)

    def expire_routes(self):
        """
        Clears out expired routes from table.
        accordingly.
        """
        # TODO: fill this in!
        # 遍历路由表中的所有路由
        for dst, entry in list(self.table.items()):
            # 如果路由已过期，从路由表中删除该路由
            if entry.expire_time < api.current_time():
                del self.table[dst]

    def handle_route_advertisement(self, route_dst, route_latency, port):
        """
        Called when the router receives a route advertisement from a neighbor.

        :param route_dst: the destination of the advertised route.
        :param route_latency: latency from the neighbor to the destination.
        :param port: the port that the advertisement arrived on.
        :return: nothing.
        """
            # 检查是否是 Poison Reverse 广播
        is_poisoned = (route_latency == INFINITY)

        existing_entry = self.table.get(route_dst)

        if is_poisoned:
            # 如果收到的广播是 Poison Reverse 广播
            if existing_entry and existing_entry.port == port:
                # 如果现有路由通过该端口，并且收到的是 Poison Reverse 广播
                # 则更新路由为不可达，但不重置过期时间
                self.table[route_dst] = TableEntry(
                    dst=route_dst,
                    port=port,
                    latency=INFINITY,
                    expire_time=existing_entry.expire_time  # 不重置过期时间
                )
                # 发送路由更新以传播 Poison Reverse
                self.send_routes()
            else:
                # 如果收到的 Poison Reverse 广播不匹配现有路由，忽略
                pass
            # TODO: fill this in!
        else:
            # 如果不是 Poison Reverse 广播
            if route_dst in self.table:
                # 如果目的地址在路由表中，更新路由表中的路由
                if self.table[route_dst].latency > route_latency + self.ports.get_latency(port):
                    # 如果新的路由延迟小于或等于旧路由延迟，更新路由表中的路由
                    latency = route_latency + self.ports.get_latency(port)
                    expire_time = api.current_time() + self.ROUTE_TTL
                    self.table[route_dst] = TableEntry(dst=route_dst, port=port, latency=latency, expire_time=expire_time)
                else:
                    # 如果新的路由延迟大于旧路由延迟
                    if self.table[route_dst].port == port:
                        # 如果新路由来自当前路由的端口，更新路由表中的路由
                        latency = route_latency + self.ports.get_latency(port)
                        expire_time = api.current_time() + self.ROUTE_TTL
                        self.table[route_dst] = TableEntry(dst=route_dst, port=port, latency=latency, expire_time=expire_time)
                    else:
                        # 如果新路由来自其他端口，不更新路由表中的路由
                        pass
            else:
                # 如果目的地址不在路由表中，添加新的路由条目
                self.table[route_dst] = TableEntry(dst=route_dst, port=port, latency=route_latency + self.ports.get_latency(port),
                                                    expire_time=api.current_time() + self.ROUTE_TTL)
    def handle_link_up(self, port, latency):
        """
        Called by the framework when a link attached to this router goes up.

        :param port: the port that the link is attached to.
        :param latency: the link latency.
        :returns: nothing.
        """
        self.ports.add_port(port, latency)

        # TODO: fill in the rest!

    def handle_link_down(self, port):
        """
        Called by the framework when a link attached to this router does down.

        :param port: the port number used by the link.
        :returns: nothing.
        """
        self.ports.remove_port(port)

        # TODO: fill this in!

    # Feel free to add any helper methods!
